77using System . Threading ;
88using System . Threading . Tasks ;
99using Microsoft . OpenApi . Reader ;
10- using SharpYaml . Serialization ;
10+ using SharpYaml ;
1111using System ;
12- using System . Linq ;
1312using System . Text ;
1413
1514namespace Microsoft . OpenApi . YamlReader
1615{
1716 /// <summary>
1817 /// Reader for parsing YAML files into an OpenAPI document.
1918 /// </summary>
19+ /// <remarks>
20+ /// Input is converted directly from SharpYaml parser events so resource limits are enforced
21+ /// before SharpYaml's recursive YAML model loader can compose or expand the document.
22+ /// </remarks>
2023 public class OpenApiYamlReader : IOpenApiReader
2124 {
2225 private const int copyBufferSize = 4096 ;
2326 private static readonly OpenApiJsonReader _jsonReader = new ( ) ;
27+ private readonly OpenApiYamlReaderSettings _yamlSettings ;
28+
29+ /// <summary>
30+ /// Initializes a YAML reader using the current legacy global conversion limits.
31+ /// </summary>
32+ public OpenApiYamlReader ( )
33+ : this ( new ( )
34+ {
35+ MaxDepth = YamlConverter . MaxDepth ,
36+ MaxNodeCount = YamlConverter . MaxNodeCount ,
37+ MaxAliasExpansionNodeCount = YamlConverter . MaxAliasExpansionNodeCount ,
38+ } )
39+ {
40+ }
41+
42+ /// <summary>
43+ /// Initializes a YAML reader with immutable per-reader resource limits.
44+ /// </summary>
45+ /// <param name="settings">The YAML reader settings.</param>
46+ public OpenApiYamlReader ( OpenApiYamlReaderSettings settings )
47+ {
48+ if ( settings is null ) throw new ArgumentNullException ( nameof ( settings ) ) ;
49+ settings . Validate ( ) ;
50+ _yamlSettings = new ( )
51+ {
52+ MaxDepth = settings . MaxDepth ,
53+ MaxNodeCount = settings . MaxNodeCount ,
54+ MaxAliasExpansionNodeCount = settings . MaxAliasExpansionNodeCount ,
55+ MaxInputByteCount = settings . MaxInputByteCount ,
56+ MaxScalarLength = settings . MaxScalarLength ,
57+ } ;
58+ }
2459
2560 /// <inheritdoc/>
2661 public async Task < ReadResult > ReadAsync ( Stream input ,
@@ -29,65 +64,122 @@ public async Task<ReadResult> ReadAsync(Stream input,
2964 CancellationToken cancellationToken = default )
3065 {
3166 if ( input is null ) throw new ArgumentNullException ( nameof ( input ) ) ;
67+ if ( settings is null ) throw new ArgumentNullException ( nameof ( settings ) ) ;
3268 if ( input is MemoryStream memoryStream )
3369 {
34- return UpdateFormat ( Read ( memoryStream , location , settings ) ) ;
70+ return ReadCore ( memoryStream , location , settings , cancellationToken ) ;
3571 }
3672 else
3773 {
3874 using var preparedStream = new MemoryStream ( ) ;
39- await input . CopyToAsync ( preparedStream , copyBufferSize , cancellationToken ) . ConfigureAwait ( false ) ;
75+ try
76+ {
77+ await CopyToMemoryStreamAsync (
78+ input ,
79+ preparedStream ,
80+ _yamlSettings . MaxInputByteCount ,
81+ cancellationToken ) . ConfigureAwait ( false ) ;
82+ }
83+ catch ( OpenApiReaderException ex )
84+ {
85+ return new ( )
86+ {
87+ Document = null ,
88+ Diagnostic = CreateDiagnostic ( new ( ex ) ) ,
89+ } ;
90+ }
91+
4092 preparedStream . Position = 0 ;
41- return UpdateFormat ( Read ( preparedStream , location , settings ) ) ;
93+ return ReadCore ( preparedStream , location , settings , cancellationToken ) ;
4294 }
4395 }
4496
4597 /// <inheritdoc/>
4698 public ReadResult Read ( MemoryStream input ,
4799 Uri location ,
48100 OpenApiReaderSettings settings )
101+ => ReadCore ( input , location , settings , CancellationToken . None ) ;
102+
103+ private ReadResult ReadCore ( MemoryStream input ,
104+ Uri location ,
105+ OpenApiReaderSettings settings ,
106+ CancellationToken cancellationToken )
49107 {
50108 if ( input is null ) throw new ArgumentNullException ( nameof ( input ) ) ;
51109 if ( settings is null ) throw new ArgumentNullException ( nameof ( settings ) ) ;
110+ cancellationToken . ThrowIfCancellationRequested ( ) ;
52111 JsonNode jsonNode ;
53112
54113 // Parse the YAML text in the stream into a sequence of JsonNodes
55114 try
56115 {
116+ EnsureInputWithinLimit ( input , _yamlSettings . MaxInputByteCount ) ;
57117#if NET
58118// this represents net core, net5 and up
59119 using var stream = new StreamReader ( input , default , true , - 1 , settings . LeaveStreamOpen ) ;
60120#else
61121// the implementation differs and results in a null reference exception in NETFX
62122 using var stream = new StreamReader ( input , Encoding . UTF8 , true , 4096 , settings . LeaveStreamOpen ) ;
63123#endif
64- jsonNode = LoadJsonNodesFromYamlDocument ( stream ) ;
124+ jsonNode = LoadJsonNodesFromYamlDocument ( stream , cancellationToken ) ;
65125 }
66126 catch ( JsonException ex )
67127 {
68- var diagnostic = new OpenApiDiagnostic ( ) ;
69- diagnostic . Errors . Add ( new ( $ "#line={ ex . LineNumber } ", ex . Message ) ) ;
70- diagnostic . Format = OpenApiConstants . Yaml ;
71128 return new ( )
72129 {
73130 Document = null ,
74- Diagnostic = diagnostic ,
131+ Diagnostic = CreateDiagnostic ( new ( $ "#line= { ex . LineNumber } " , ex . Message ) ) ,
75132 } ;
76133 }
77134 catch ( OpenApiReaderException ex )
78135 {
79- var diagnostic = new OpenApiDiagnostic ( ) ;
80- diagnostic . Errors . Add ( new ( ex ) ) ;
81- diagnostic . Format = OpenApiConstants . Yaml ;
82136 return new ( )
83137 {
84138 Document = null ,
85- Diagnostic = diagnostic ,
139+ Diagnostic = CreateDiagnostic ( new ( ex ) ) ,
86140 } ;
87141 }
88142
143+ cancellationToken . ThrowIfCancellationRequested ( ) ;
89144 return UpdateFormat ( Read ( jsonNode , location , settings ) ) ;
90145 }
146+
147+ private static async Task CopyToMemoryStreamAsync (
148+ Stream input ,
149+ MemoryStream output ,
150+ uint maxInputByteCount ,
151+ CancellationToken cancellationToken )
152+ {
153+ var buffer = new byte [ copyBufferSize ] ;
154+ long totalBytesRead = 0 ;
155+ int bytesRead ;
156+ while ( ( bytesRead = await input . ReadAsync (
157+ buffer ,
158+ 0 ,
159+ buffer . Length ,
160+ cancellationToken ) . ConfigureAwait ( false ) ) > 0 )
161+ {
162+ if ( bytesRead > ( long ) maxInputByteCount - totalBytesRead )
163+ {
164+ throw CreateInputLimitException ( maxInputByteCount ) ;
165+ }
166+
167+ await output . WriteAsync ( buffer , 0 , bytesRead , cancellationToken ) . ConfigureAwait ( false ) ;
168+ totalBytesRead += bytesRead ;
169+ }
170+ }
171+
172+ private static void EnsureInputWithinLimit ( MemoryStream input , uint maxInputByteCount )
173+ {
174+ if ( input . Length - input . Position > maxInputByteCount )
175+ {
176+ throw CreateInputLimitException ( maxInputByteCount ) ;
177+ }
178+ }
179+
180+ private static OpenApiReaderException CreateInputLimitException ( uint maxInputByteCount )
181+ => new ( $ "The YAML input exceeds the maximum supported size of { maxInputByteCount } bytes.") ;
182+
91183 private static ReadResult UpdateFormat ( ReadResult result )
92184 {
93185 result . Diagnostic ??= new OpenApiDiagnostic ( ) ;
@@ -114,13 +206,22 @@ public static ReadResult Read(JsonNode jsonNode, Uri location, OpenApiReaderSett
114206 // Parse the YAML
115207 try
116208 {
117- using var stream = new StreamReader ( input ) ;
118- jsonNode = LoadJsonNodesFromYamlDocument ( stream ) ;
209+ EnsureInputWithinLimit ( input , _yamlSettings . MaxInputByteCount ) ;
210+ #if NET
211+ using var stream = new StreamReader ( input , default , true , - 1 , settings ? . LeaveStreamOpen ?? false ) ;
212+ #else
213+ using var stream = new StreamReader ( input , Encoding . UTF8 , true , 4096 , settings ? . LeaveStreamOpen ?? false ) ;
214+ #endif
215+ jsonNode = LoadJsonNodesFromYamlDocument ( stream , CancellationToken . None ) ;
119216 }
120217 catch ( JsonException ex )
121218 {
122- diagnostic = new ( ) ;
123- diagnostic . Errors . Add ( new ( $ "#line={ ex . LineNumber } ", ex . Message ) ) ;
219+ diagnostic = CreateDiagnostic ( new ( $ "#line={ ex . LineNumber } ", ex . Message ) ) ;
220+ return default ;
221+ }
222+ catch ( OpenApiReaderException ex )
223+ {
224+ diagnostic = CreateDiagnostic ( new ( ex ) ) ;
124225 return default ;
125226 }
126227
@@ -134,20 +235,34 @@ public static ReadResult Read(JsonNode jsonNode, Uri location, OpenApiReaderSett
134235 }
135236
136237 /// <summary>
137- /// Helper method to turn streams into a sequence of JsonNodes
238+ /// Converts the first YAML document in a stream into a JSON node.
138239 /// </summary>
139240 /// <param name="input">Stream containing YAML formatted text</param>
140- /// <returns>Instance of a YamlDocument</returns>
141- static JsonNode LoadJsonNodesFromYamlDocument ( TextReader input )
241+ /// <param name="cancellationToken">Propagates notification that parsing should be cancelled.</param>
242+ /// <returns>The converted JSON node.</returns>
243+ private JsonNode LoadJsonNodesFromYamlDocument ( TextReader input , CancellationToken cancellationToken )
142244 {
143- var yamlStream = new YamlStream ( ) ;
144- yamlStream . Load ( input ) ;
145- if ( yamlStream . Documents . Any ( ) && yamlStream . Documents [ 0 ] . ToJsonNode ( ) is { } jsonNode )
245+ try
146246 {
147- return jsonNode ;
247+ return new YamlJsonParser ( _yamlSettings ) . Parse ( input , cancellationToken ) ;
148248 }
249+ catch ( YamlException ex )
250+ {
251+ var location = ex . Start . Line >= 0
252+ ? $ " at line { ex . Start . Line + 1 } , column { ex . Start . Column + 1 } "
253+ : string . Empty ;
254+ throw new OpenApiReaderException ( $ "Unable to parse the YAML document{ location } : { ex . Message } ", ex ) ;
255+ }
256+ }
149257
150- throw new InvalidOperationException ( "No documents found in the YAML stream." ) ;
258+ private static OpenApiDiagnostic CreateDiagnostic ( OpenApiError error )
259+ {
260+ var diagnostic = new OpenApiDiagnostic
261+ {
262+ Format = OpenApiConstants . Yaml ,
263+ } ;
264+ diagnostic . Errors . Add ( error ) ;
265+ return diagnostic ;
151266 }
152267 }
153268}
0 commit comments